1   // Licensed under the Apache License, Version 2.0 (the "License");
2   // you may not use this file except in compliance with the License.
3   // You may obtain a copy of the License at
4   //
5   //     http://www.apache.org/licenses/LICENSE-2.0
6   //
7   // Unless required by applicable law or agreed to in writing, software
8   // distributed under the License is distributed on an "AS IS" BASIS,
9   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  // See the License for the specific language governing permissions and
11  // limitations under the License.
12  
13  package org.apache.tapestry5.internal.services;
14  
15  import org.apache.tapestry5.internal.parser.*;
16  import org.apache.tapestry5.ioc.Location;
17  import org.apache.tapestry5.ioc.Resource;
18  import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
19  import org.apache.tapestry5.ioc.internal.util.InternalUtils;
20  import org.apache.tapestry5.ioc.internal.util.TapestryException;
21  import org.apache.tapestry5.ioc.util.ExceptionUtils;
22  
23  import javax.xml.namespace.QName;
24  import java.net.URL;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.regex.Matcher;
29  import java.util.regex.Pattern;
30  
31  import static org.apache.tapestry5.internal.services.SaxTemplateParser.Version.*;
32  
33  /**
34   * SAX-based template parser logic, taking a {@link Resource} to a Tapestry
35   * template file and returning
36   * a {@link ComponentTemplate}.
37   *
38   * Earlier versions of this code used the StAX (streaming XML parser), but that
39   * was really, really bad for Google App Engine. This version uses SAX under the
40   * covers, but kind of replicates the important bits of the StAX API as
41   * {@link XMLTokenStream}.
42   *
43   * @since 5.2.0
44   */
45  @SuppressWarnings(
46          {"JavaDoc"})
47  public class SaxTemplateParser
48  {
49      private static final String MIXINS_ATTRIBUTE_NAME = "mixins";
50  
51      private static final String TYPE_ATTRIBUTE_NAME = "type";
52  
53      private static final String ID_ATTRIBUTE_NAME = "id";
54  
55      public static final String XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace";
56  
57      private static final Map<String, Version> NAMESPACE_URI_TO_VERSION = CollectionFactory.newMap();
58  
59      {
60          NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_0_0.xsd", T_5_0);
61          NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_1_0.xsd", T_5_1);
62          // 5.2 didn't change the schmea, so the 5_1_0.xsd was still used.
63          // 5.3 fixes an incorrect element name in the XSD ("replacement" should be "replace")
64          // The parser code here always expected "replace".
65          NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_3.xsd", T_5_3);
66          // 5.4 is pretty much the same as 5.3, but allows block inside extend
67          // as per TAP5-1847
68          NAMESPACE_URI_TO_VERSION.put("http://tapestry.apache.org/schema/tapestry_5_4.xsd", T_5_4);
69      }
70  
71      /**
72       * Special namespace used to denote Block parameters to components, as a
73       * (preferred) alternative to the t:parameter
74       * element. The simple element name is the name of the parameter.
75       */
76      private static final String TAPESTRY_PARAMETERS_URI = "tapestry:parameter";
77  
78      /**
79       * URI prefix used to identify a Tapestry library, the remainder of the URI
80       * becomes a prefix on the element name.
81       */
82      private static final String LIB_NAMESPACE_URI_PREFIX = "tapestry-library:";
83  
84      /**
85       * Pattern used to parse the path portion of the library namespace URI. A
86       * series of simple identifiers with slashes
87       * allowed as seperators.
88       */
89  
90      private static final Pattern LIBRARY_PATH_PATTERN = Pattern.compile("^[a-z]\\w*(/[a-z]\\w*)*$",
91              Pattern.CASE_INSENSITIVE);
92  
93      private static final Pattern ID_PATTERN = Pattern.compile("^[a-z]\\w*$",
94              Pattern.CASE_INSENSITIVE);
95  
96      /**
97       * Any amount of mixed simple whitespace (space, tab, form feed) mixed with
98       * at least one carriage return or line
99       * feed, followed by any amount of whitespace. Will be reduced to a single
100      * linefeed.
101      */
102     private static final Pattern REDUCE_LINEBREAKS_PATTERN = Pattern.compile(
103             "[ \\t\\f]*[\\r\\n]\\s*", Pattern.MULTILINE);
104 
105     /**
106      * Used when compressing whitespace, matches any sequence of simple
107      * whitespace (space, tab, formfeed). Applied after
108      * REDUCE_LINEBREAKS_PATTERN.
109      */
110     private static final Pattern REDUCE_WHITESPACE_PATTERN = Pattern.compile("[ \\t\\f]+",
111             Pattern.MULTILINE);
112 
113     // Note the use of the non-greedy modifier; this prevents the pattern from
114     // merging multiple
115     // expansions on the same text line into a single large
116     // but invalid expansion.
117 
118     private static final Pattern EXPANSION_PATTERN = Pattern.compile("\\$\\{\\s*(((?!\\$\\{).)*)\\s*}");
119     private static final char EXPANSION_STRING_DELIMITTER = '\'';
120     private static final char OPEN_BRACE = '{';
121     private static final char CLOSE_BRACE = '}';
122 
123     private static final Set<String> MUST_BE_ROOT = CollectionFactory.newSet("extend", "container");
124 
125     private final Resource resource;
126 
127     private final XMLTokenStream tokenStream;
128 
129     private final StringBuilder textBuffer = new StringBuilder();
130 
131     private final List<TemplateToken> tokens = CollectionFactory.newList();
132 
133     // This starts pointing at tokens but occasionally shifts to a list inside
134     // the overrides Map.
135     private List<TemplateToken> tokenAccumulator = tokens;
136 
137     /**
138      * Primarily used as a set of componentIds (to check for duplicates and
139      * conflicts).
140      */
141     private final Map<String, Location> componentIds = CollectionFactory.newCaseInsensitiveMap();
142 
143     /**
144      * Map from override id to a list of tokens; this actually works both for
145      * overrides defined by this template and
146      * overrides provided by this template.
147      */
148     private Map<String, List<TemplateToken>> overrides;
149 
150     private boolean extension;
151 
152     private Location textStartLocation;
153 
154     private boolean active = true;
155 
156     private boolean strictMixinParameters = false;
157 
158     private final Map<String, Boolean> extensionPointIdSet = CollectionFactory.newCaseInsensitiveMap();
159 
160     public SaxTemplateParser(Resource resource, Map<String, URL> publicIdToURL)
161     {
162         this.resource = resource;
163         this.tokenStream = new XMLTokenStream(resource, publicIdToURL);
164     }
165 
166     public ComponentTemplate parse(boolean compressWhitespace)
167     {
168         try
169         {
170             tokenStream.parse();
171 
172             TemplateParserState initialParserState = new TemplateParserState()
173                     .compressWhitespace(compressWhitespace);
174 
175             root(initialParserState);
176 
177             return new ComponentTemplateImpl(resource, tokens, componentIds, extension, strictMixinParameters, overrides);
178         } catch (Exception ex)
179         {
180             throw new TapestryException(String.format("Failure parsing template %s: %s", resource,
181                     ExceptionUtils.toMessage(ex)), tokenStream.getLocation(), ex);
182         }
183 
184     }
185 
186     void root(TemplateParserState state)
187     {
188         while (active && tokenStream.hasNext())
189         {
190             switch (tokenStream.next())
191             {
192                 case DTD:
193 
194                     dtd();
195 
196                     break;
197 
198                 case START_ELEMENT:
199 
200                     rootElement(state);
201 
202                     break;
203 
204                 case END_DOCUMENT:
205                     // Ignore it.
206                     break;
207 
208                 default:
209                     textContent(state);
210             }
211         }
212     }
213 
214     private void rootElement(TemplateParserState initialState)
215     {
216         TemplateParserState state = setupForElement(initialState);
217 
218         String uri = tokenStream.getNamespaceURI();
219         String name = tokenStream.getLocalName();
220         Version version = NAMESPACE_URI_TO_VERSION.get(uri);
221 
222         if (T_5_1.sameOrEarlier(version))
223         {
224             if (name.equalsIgnoreCase("extend"))
225             {
226                 extend(state);
227                 return;
228             }
229         }
230 
231         if (version != null)
232         {
233             if (name.equalsIgnoreCase("container"))
234             {
235                 container(state);
236                 return;
237             }
238         }
239 
240         element(state);
241     }
242 
243     private void extend(TemplateParserState state)
244     {
245         extension = true;
246 
247         while (active)
248         {
249             switch (tokenStream.next())
250             {
251                 case START_ELEMENT:
252 
253                     if (isTemplateVersion(Version.T_5_1) && isElementName("replace"))
254                     {
255                         replace(state);
256                         break;
257                     }
258 
259                     boolean is54 = isTemplateVersion(Version.T_5_4);
260 
261                     if (is54 && isElementName("block"))
262                     {
263                         block(state);
264                         break;
265                     }
266 
267                     throw new RuntimeException(
268                             is54
269                                     ? "Child element of <extend> must be <replace> or <block>."
270                                     : "Child element of <extend> must be <replace>.");
271 
272                 case END_ELEMENT:
273 
274                     return;
275 
276                 // Ignore spaces and comments directly inside <extend>.
277 
278                 case COMMENT:
279                 case SPACE:
280                     break;
281 
282                 // Other non-whitespace content (characters, etc.) are forbidden.
283 
284                 case CHARACTERS:
285                     if (InternalUtils.isBlank(tokenStream.getText()))
286                         break;
287 
288                 default:
289                     unexpectedEventType();
290             }
291         }
292     }
293 
294     /**
295      * Returns true if the <em>local name</em> is the element name (ignoring case).
296      */
297     private boolean isElementName(String elementName)
298     {
299         return tokenStream.getLocalName().equalsIgnoreCase(elementName);
300     }
301 
302     /**
303      * Returns true if the template version is at least the required version.
304      */
305     private boolean isTemplateVersion(Version requiredVersion)
306     {
307         Version templateVersion = NAMESPACE_URI_TO_VERSION.get(tokenStream.getNamespaceURI());
308 
309         return requiredVersion.sameOrEarlier(templateVersion);
310     }
311 
312     private void replace(TemplateParserState state)
313     {
314         String id = getRequiredIdAttribute();
315 
316         addContentToOverride(setupForElement(state), id);
317     }
318 
319     private void unexpectedEventType()
320     {
321         XMLTokenType eventType = tokenStream.getEventType();
322 
323         throw new IllegalStateException(String.format("Unexpected XML parse event %s.", eventType
324                 .name()));
325     }
326 
327     private void dtd()
328     {
329         DTDData dtdInfo = tokenStream.getDTDInfo();
330 
331         tokenAccumulator.add(new DTDToken(dtdInfo.rootName, dtdInfo.publicId, dtdInfo
332                 .systemId, getLocation()));
333     }
334 
335     private Location getLocation()
336     {
337         return tokenStream.getLocation();
338     }
339 
340     /**
341      * Processes an element through to its matching end tag.
342      *
343      * An element can be:
344      *
345      * a Tapestry component via &lt;t:type&gt;
346      *
347      * a Tapestry component via t:type="type" and/or t:id="id"
348      *
349      * a Tapestry component via a library namespace
350      *
351      * A parameter element via &lt;t:parameter&gt;
352      *
353      * A parameter element via &lt;p:name&gt;
354      *
355      * A &lt;t:remove&gt; element (in the 5.1 schema)
356      *
357      * A &lt;t:content&gt; element (in the 5.1 schema)
358      *
359      * A &lt;t:block&gt; element
360      *
361      * The body &lt;t:body&gt;
362      *
363      * An ordinary element
364      */
365     void element(TemplateParserState initialState)
366     {
367         TemplateParserState state = setupForElement(initialState);
368 
369         String uri = tokenStream.getNamespaceURI();
370         String name = tokenStream.getLocalName();
371         Version version = NAMESPACE_URI_TO_VERSION.get(uri);
372 
373         if (T_5_1.sameOrEarlier(version))
374         {
375 
376             if (name.equalsIgnoreCase("remove"))
377             {
378                 removeContent();
379 
380                 return;
381             }
382 
383             if (name.equalsIgnoreCase("content"))
384             {
385                 limitContent(state);
386 
387                 return;
388             }
389 
390             if (name.equalsIgnoreCase("extension-point"))
391             {
392                 extensionPoint(state);
393 
394                 return;
395             }
396 
397             if (name.equalsIgnoreCase("replace"))
398             {
399                 throw new RuntimeException(
400                         "The <replace> element may only appear directly within an extend element.");
401             }
402 
403             if (MUST_BE_ROOT.contains(name))
404                 mustBeRoot(name);
405         }
406 
407         if (version != null)
408         {
409 
410             if (name.equalsIgnoreCase("body"))
411             {
412                 body();
413                 return;
414             }
415 
416             if (name.equalsIgnoreCase("container"))
417             {
418                 mustBeRoot(name);
419             }
420 
421             if (name.equalsIgnoreCase("block"))
422             {
423                 block(state);
424                 return;
425             }
426 
427             if (name.equalsIgnoreCase("parameter"))
428             {
429                 if (T_5_3.sameOrEarlier(version))
430                 {
431                     throw new RuntimeException(
432                             String.format("The <parameter> element has been deprecated in Tapestry 5.3 in favour of '%s' namespace.", TAPESTRY_PARAMETERS_URI));
433                 }
434 
435                 classicParameter(state);
436 
437                 return;
438             }
439 
440             possibleTapestryComponent(state, null, tokenStream.getLocalName().replace('.', '/'));
441 
442             return;
443         }
444 
445         if (uri != null && uri.startsWith(LIB_NAMESPACE_URI_PREFIX))
446         {
447             libraryNamespaceComponent(state);
448 
449             return;
450         }
451 
452         if (TAPESTRY_PARAMETERS_URI.equals(uri))
453         {
454             parameterElement(state);
455 
456             return;
457         }
458 
459         // Just an ordinary element ... unless it has t:id or t:type
460 
461         possibleTapestryComponent(state, tokenStream.getLocalName(), null);
462     }
463 
464     /**
465      * Processes a body of an element including text and (recursively) nested
466      * elements. Adds an
467      * {@link org.apache.tapestry5.internal.parser.TokenType#END_ELEMENT} token
468      * before returning.
469      *
470      * @param state
471      */
472     private void processBody(TemplateParserState state)
473     {
474         while (active)
475         {
476             switch (tokenStream.next())
477             {
478                 case START_ELEMENT:
479 
480                     // The recursive part: when we see a new element start.
481 
482                     element(state);
483                     break;
484 
485                 case END_ELEMENT:
486 
487                     // At the end of an element, we're done and can return.
488                     // This is the matching end element for the start element
489                     // that invoked this method.
490 
491                     endElement(state);
492 
493                     return;
494 
495                 default:
496                     textContent(state);
497             }
498         }
499     }
500 
501     private TemplateParserState setupForElement(TemplateParserState initialState)
502     {
503         processTextBuffer(initialState);
504 
505         return checkForXMLSpaceAttribute(initialState);
506     }
507 
508     /**
509      * Handles an extension point, putting a RenderExtension token in position
510      * in the template.
511      *
512      * @param state
513      */
514     private void extensionPoint(TemplateParserState state)
515     {
516         // An extension point adds a token that represents where the override
517         // (either the default
518         // provided in the parent template, or the true override from a child
519         // template) is positioned.
520 
521         String id = getRequiredIdAttribute();
522 
523         if (extensionPointIdSet.containsKey(id))
524         {
525             throw new TapestryException(String.format("Extension point '%s' is already defined for this template. Extension point ids must be unique.", id), getLocation(), null);
526         } else
527         {
528             extensionPointIdSet.put(id, true);
529         }
530 
531         tokenAccumulator.add(new ExtensionPointToken(id, getLocation()));
532 
533         addContentToOverride(state.insideComponent(false), id);
534     }
535 
536     private String getRequiredIdAttribute()
537     {
538         String id = getSingleParameter("id");
539 
540         if (InternalUtils.isBlank(id))
541             throw new RuntimeException(String.format("The <%s> element must have an id attribute.",
542                     tokenStream.getLocalName()));
543 
544         return id;
545     }
546 
547     private void addContentToOverride(TemplateParserState state, String id)
548 
549     {
550         List<TemplateToken> savedTokenAccumulator = tokenAccumulator;
551 
552         tokenAccumulator = CollectionFactory.newList();
553 
554         // TODO: id should probably be unique; i.e., you either define an
555         // override or you
556         // provide an override, but you don't do both in the same template.
557 
558         if (overrides == null)
559             overrides = CollectionFactory.newCaseInsensitiveMap();
560 
561         overrides.put(id, tokenAccumulator);
562 
563         while (active)
564         {
565             switch (tokenStream.next())
566             {
567                 case START_ELEMENT:
568                     element(state);
569                     break;
570 
571                 case END_ELEMENT:
572 
573                     processTextBuffer(state);
574 
575                     // Restore everthing to how it was before the
576                     // extention-point was reached.
577 
578                     tokenAccumulator = savedTokenAccumulator;
579                     return;
580 
581                 default:
582                     textContent(state);
583             }
584         }
585     }
586 
587     private void mustBeRoot(String name)
588     {
589         throw new RuntimeException(String.format(
590                 "Element <%s> is only valid as the root element of a template.", name));
591     }
592 
593     /**
594      * Triggered by &lt;t:content&gt; element; limits template content to just
595      * what's inside.
596      */
597 
598     private void limitContent(TemplateParserState state)
599     {
600         if (state.isCollectingContent())
601             throw new IllegalStateException(
602                     "The <content> element may not be nested within another <content> element.");
603 
604         TemplateParserState newState = state.collectingContent().insideComponent(false);
605 
606         // Clear out any tokens that precede the <t:content> element
607 
608         tokens.clear();
609 
610         // I'm not happy about this; you really shouldn't define overrides just
611         // to clear them out,
612         // but it is consistent. Perhaps this should be an error if overrides is
613         // non-empty.
614 
615         overrides = null;
616 
617         // Make sure that if the <t:content> appears inside a <t:replace> or
618         // <t:extension-point>, that
619         // it is still handled correctly.
620 
621         tokenAccumulator = tokens;
622 
623         while (active)
624         {
625             switch (tokenStream.next())
626             {
627                 case START_ELEMENT:
628                     element(newState);
629                     break;
630 
631                 case END_ELEMENT:
632 
633                     // The active flag is global, once we hit it, the entire
634                     // parse is aborted, leaving
635                     // tokens with just tokens defined inside <t:content>.
636 
637                     processTextBuffer(newState);
638 
639                     active = false;
640 
641                     break;
642 
643                 default:
644                     textContent(state);
645             }
646         }
647 
648     }
649 
650     private void removeContent()
651     {
652         int depth = 1;
653 
654         while (active)
655         {
656             switch (tokenStream.next())
657             {
658                 case START_ELEMENT:
659                     depth++;
660                     break;
661 
662                 // The matching end element.
663 
664                 case END_ELEMENT:
665                     depth--;
666 
667                     if (depth == 0)
668                         return;
669 
670                     break;
671 
672                 default:
673                     // Ignore anything else (text, comments, etc.)
674             }
675         }
676     }
677 
678     private String nullForBlank(String input)
679     {
680         return InternalUtils.isBlank(input) ? null : input;
681     }
682 
683     /**
684      * Added in release 5.1.
685      */
686     private void libraryNamespaceComponent(TemplateParserState state)
687     {
688         String uri = tokenStream.getNamespaceURI();
689 
690         // The library path is encoded into the namespace URI.
691 
692         String path = uri.substring(LIB_NAMESPACE_URI_PREFIX.length());
693 
694         if (!LIBRARY_PATH_PATTERN.matcher(path).matches())
695             throw new RuntimeException(String.format("The path portion of library namespace URI '%s' is not valid: it must be a simple identifier, or a series of identifiers seperated by slashes.", uri));
696 
697         possibleTapestryComponent(state, null, path + "/" + tokenStream.getLocalName());
698     }
699 
700     /**
701      * @param elementName
702      * @param identifiedType
703      *         the type of the element, usually null, but may be the
704      *         component type derived from element
705      */
706     private void possibleTapestryComponent(TemplateParserState state, String elementName,
707                                            String identifiedType)
708     {
709         String id = null;
710         String type = identifiedType;
711         String mixins = null;
712 
713         int count = tokenStream.getAttributeCount();
714 
715         Location location = getLocation();
716 
717         List<TemplateToken> attributeTokens = CollectionFactory.newList();
718 
719         for (int i = 0; i < count; i++)
720         {
721             QName qname = tokenStream.getAttributeName(i);
722 
723             if (isXMLSpaceAttribute(qname))
724                 continue;
725 
726             // The name will be blank for an xmlns: attribute
727 
728             String localName = qname.getLocalPart();
729 
730             if (InternalUtils.isBlank(localName))
731                 continue;
732 
733             String uri = qname.getNamespaceURI();
734 
735             String value = tokenStream.getAttributeValue(i);
736 
737 
738             Version version = NAMESPACE_URI_TO_VERSION.get(uri);
739 
740             if (version != null)
741             {
742                 // We are kind of assuming that the namespace URI appears once, in the outermost element of the template.
743                 // And we don't and can't handle the case that it appears multiple times in the template.
744 
745                 if (T_5_4.sameOrEarlier(version)) {
746                     strictMixinParameters = true;
747                 }
748 
749                 if (localName.equalsIgnoreCase(ID_ATTRIBUTE_NAME))
750                 {
751                     id = nullForBlank(value);
752 
753                     validateId(id, "Component id '%s' is not valid; component ids must be valid Java identifiers: start with a letter, and consist of letters, numbers and underscores.");
754 
755                     continue;
756                 }
757 
758                 if (type == null && localName.equalsIgnoreCase(TYPE_ATTRIBUTE_NAME))
759                 {
760                     type = nullForBlank(value);
761                     continue;
762                 }
763 
764                 if (localName.equalsIgnoreCase(MIXINS_ATTRIBUTE_NAME))
765                 {
766                     mixins = nullForBlank(value);
767                     continue;
768                 }
769 
770                 // Anything else is the name of a Tapestry component parameter
771                 // that is simply
772                 // not part of the template's doctype for the element being
773                 // instrumented.
774             }
775 
776             attributeTokens.add(new AttributeToken(uri, localName, value, location));
777         }
778 
779         boolean isComponent = (id != null || type != null);
780 
781         // If provided t:mixins but not t:id or t:type, then its not quite a
782         // component
783 
784         if (mixins != null && !isComponent)
785             throw new TapestryException(String.format("You may not specify mixins for element <%s> because it does not represent a component (which requires either an id attribute or a type attribute).", elementName),
786                     location, null);
787 
788         if (isComponent)
789         {
790             tokenAccumulator.add(new StartComponentToken(elementName, id, type, mixins, location));
791         } else
792         {
793             tokenAccumulator.add(new StartElementToken(tokenStream.getNamespaceURI(), elementName,
794                     location));
795         }
796 
797         addDefineNamespaceTokens();
798 
799         tokenAccumulator.addAll(attributeTokens);
800 
801         if (id != null)
802             componentIds.put(id, location);
803 
804         processBody(state.insideComponent(isComponent));
805     }
806 
807     private void addDefineNamespaceTokens()
808     {
809         for (int i = 0; i < tokenStream.getNamespaceCount(); i++)
810         {
811             String uri = tokenStream.getNamespaceURI(i);
812 
813             // These URIs are strictly part of the server-side Tapestry template
814             // and are not ever sent to the client.
815 
816             if (NAMESPACE_URI_TO_VERSION.containsKey(uri))
817                 continue;
818 
819             if (uri.equals(TAPESTRY_PARAMETERS_URI))
820                 continue;
821 
822             if (uri.startsWith(LIB_NAMESPACE_URI_PREFIX))
823                 continue;
824 
825             tokenAccumulator.add(new DefineNamespacePrefixToken(uri, tokenStream
826                     .getNamespacePrefix(i), getLocation()));
827         }
828     }
829 
830     private TemplateParserState checkForXMLSpaceAttribute(TemplateParserState state)
831     {
832         for (int i = 0; i < tokenStream.getAttributeCount(); i++)
833         {
834             QName qName = tokenStream.getAttributeName(i);
835 
836             if (isXMLSpaceAttribute(qName))
837             {
838                 boolean compress = !"preserve".equals(tokenStream.getAttributeValue(i));
839 
840                 return state.compressWhitespace(compress);
841             }
842         }
843 
844         return state;
845     }
846 
847     /**
848      * Processes the text buffer and then adds an end element token.
849      */
850     private void endElement(TemplateParserState state)
851     {
852         processTextBuffer(state);
853 
854         tokenAccumulator.add(new EndElementToken(getLocation()));
855     }
856 
857     /**
858      * Handler for Tapestry 5.0's "classic" &lt;t:parameter&gt; element. This
859      * turns into a {@link org.apache.tapestry5.internal.parser.ParameterToken}
860      * and the body and end element are provided normally.
861      */
862     private void classicParameter(TemplateParserState state)
863     {
864         String parameterName = getSingleParameter("name");
865 
866         if (InternalUtils.isBlank(parameterName))
867             throw new TapestryException("The name attribute of the <parameter> element must be specified.",
868                     getLocation(), null);
869 
870         ensureParameterWithinComponent(state);
871 
872         tokenAccumulator.add(new ParameterToken(parameterName, getLocation()));
873 
874         processBody(state.insideComponent(false));
875     }
876 
877     private void ensureParameterWithinComponent(TemplateParserState state)
878     {
879         if (!state.isInsideComponent())
880             throw new RuntimeException(
881                     "Block parameters are only allowed directly within component elements.");
882     }
883 
884     /**
885      * Tapestry 5.1 uses a special namespace (usually mapped to "p:") and the
886      * name becomes the parameter element.
887      */
888     private void parameterElement(TemplateParserState state)
889     {
890         ensureParameterWithinComponent(state);
891 
892         if (tokenStream.getAttributeCount() > 0)
893             throw new TapestryException("A block parameter element does not allow any additional attributes. The element name defines the parameter name.",
894                     getLocation(), null);
895 
896         tokenAccumulator.add(new ParameterToken(tokenStream.getLocalName(), getLocation()));
897 
898         processBody(state.insideComponent(false));
899     }
900 
901     /**
902      * Checks that a body element is empty. Returns after the body's close
903      * element. Adds a single body token (but not an
904      * end token).
905      */
906     private void body()
907     {
908         tokenAccumulator.add(new BodyToken(getLocation()));
909 
910         while (active)
911         {
912             switch (tokenStream.next())
913             {
914                 case END_ELEMENT:
915                     return;
916 
917                 default:
918                     throw new IllegalStateException(String.format("Content inside a Tapestry body element is not allowed (at %s). The content has been ignored.", getLocation()));
919             }
920         }
921     }
922 
923     /**
924      * Driven by the &lt;t:container&gt; element, this state adds elements for
925      * its body but not its start or end tags.
926      *
927      * @param state
928      */
929     private void container(TemplateParserState state)
930     {
931         while (active)
932         {
933             switch (tokenStream.next())
934             {
935                 case START_ELEMENT:
936                     element(state);
937                     break;
938 
939                 // The matching end-element for the container. Don't add a
940                 // token.
941 
942                 case END_ELEMENT:
943 
944                     processTextBuffer(state);
945 
946                     return;
947 
948                 default:
949                     textContent(state);
950             }
951         }
952     }
953 
954     /**
955      * A block adds a token for its start tag and end tag and allows any content
956      * within.
957      */
958     private void block(TemplateParserState state)
959     {
960         String blockId = getSingleParameter("id");
961 
962         validateId(blockId, "Block id '%s' is not valid; block ids must be valid Java identifiers: start with a letter, and consist of letters, numbers and underscores.");
963 
964         tokenAccumulator.add(new BlockToken(blockId, getLocation()));
965 
966         processBody(state.insideComponent(false));
967     }
968 
969     private String getSingleParameter(String attributeName)
970     {
971         String result = null;
972 
973         for (int i = 0; i < tokenStream.getAttributeCount(); i++)
974         {
975             QName qName = tokenStream.getAttributeName(i);
976 
977             if (isXMLSpaceAttribute(qName))
978                 continue;
979 
980             if (qName.getLocalPart().equalsIgnoreCase(attributeName))
981             {
982                 result = tokenStream.getAttributeValue(i);
983                 continue;
984             }
985 
986             // Only the named attribute is allowed.
987 
988             throw new TapestryException(String.format("Element <%s> does not support an attribute named '%s'. The only allowed attribute name is '%s'.", tokenStream
989                     .getLocalName(), qName.toString(), attributeName), getLocation(), null);
990         }
991 
992         return result;
993     }
994 
995     private void validateId(String id, String messageKey)
996     {
997         if (id == null)
998             return;
999 
1000         if (ID_PATTERN.matcher(id).matches())
1001             return;
1002 
1003         // Not a match.
1004 
1005         throw new TapestryException(String.format(messageKey, id), getLocation(), null);
1006     }
1007 
1008     private boolean isXMLSpaceAttribute(QName qName)
1009     {
1010         return XML_NAMESPACE_URI.equals(qName.getNamespaceURI())
1011                 && "space".equals(qName.getLocalPart());
1012     }
1013 
1014     /**
1015      * Processes text content if in the correct state, or throws an exception.
1016      * This is used as a default for matching
1017      * case statements.
1018      *
1019      * @param state
1020      */
1021     private void textContent(TemplateParserState state)
1022     {
1023         switch (tokenStream.getEventType())
1024         {
1025             case COMMENT:
1026                 comment(state);
1027                 break;
1028 
1029             case CDATA:
1030                 cdata(state);
1031                 break;
1032 
1033             case CHARACTERS:
1034             case SPACE:
1035                 characters();
1036                 break;
1037 
1038             default:
1039                 unexpectedEventType();
1040         }
1041     }
1042 
1043     private void characters()
1044     {
1045         if (textStartLocation == null)
1046             textStartLocation = getLocation();
1047 
1048         textBuffer.append(tokenStream.getText());
1049     }
1050 
1051     private void cdata(TemplateParserState state)
1052     {
1053         processTextBuffer(state);
1054 
1055         tokenAccumulator.add(new CDATAToken(tokenStream.getText(), getLocation()));
1056     }
1057 
1058     private void comment(TemplateParserState state)
1059     {
1060         processTextBuffer(state);
1061 
1062         String comment = tokenStream.getText();
1063 
1064         tokenAccumulator.add(new CommentToken(comment, getLocation()));
1065     }
1066 
1067     /**
1068      * Processes the accumulated text in the text buffer as a text token.
1069      */
1070     private void processTextBuffer(TemplateParserState state)
1071     {
1072         if (textBuffer.length() != 0)
1073             convertTextBufferToTokens(state);
1074 
1075         textStartLocation = null;
1076     }
1077 
1078     private void convertTextBufferToTokens(TemplateParserState state)
1079     {
1080         String text = textBuffer.toString();
1081 
1082         textBuffer.setLength(0);
1083 
1084         if (state.isCompressWhitespace())
1085         {
1086             text = compressWhitespaceInText(text);
1087 
1088             if (InternalUtils.isBlank(text))
1089                 return;
1090         }
1091 
1092         addTokensForText(text);
1093     }
1094 
1095     /**
1096      * Reduces vertical whitespace to a single newline, then reduces horizontal
1097      * whitespace to a single space.
1098      *
1099      * @param text
1100      * @return compressed version of text
1101      */
1102     private String compressWhitespaceInText(String text)
1103     {
1104         String linebreaksReduced = REDUCE_LINEBREAKS_PATTERN.matcher(text).replaceAll("\n");
1105 
1106         return REDUCE_WHITESPACE_PATTERN.matcher(linebreaksReduced).replaceAll(" ");
1107     }
1108 
1109     /**
1110      * Scans the text, using a regular expression pattern, for expansion
1111      * patterns, and adds appropriate tokens for what
1112      * it finds.
1113      *
1114      * @param text
1115      *         to add as
1116      *         {@link org.apache.tapestry5.internal.parser.TextToken}s and
1117      *         {@link org.apache.tapestry5.internal.parser.ExpansionToken}s
1118      */
1119     private void addTokensForText(String text)
1120     {
1121         Matcher matcher = EXPANSION_PATTERN.matcher(text);
1122 
1123         int startx = 0;
1124 
1125         // The big problem with all this code is that everything gets assigned
1126         // to the
1127         // start of the text block, even if there are line breaks leading up to
1128         // it.
1129         // That's going to take a lot more work and there are bigger fish to
1130         // fry. In addition,
1131         // TAPESTRY-2028 means that the whitespace has likely been stripped out
1132         // of the text
1133         // already anyway.
1134         while (matcher.find())
1135         {
1136             int matchStart = matcher.start();
1137 
1138             if (matchStart != startx)
1139             {
1140                 String prefix = text.substring(startx, matchStart);
1141                 tokenAccumulator.add(new TextToken(prefix, textStartLocation));
1142             }
1143 
1144             // Group 1 includes the real text of the expansion, with whitespace
1145             // around the
1146             // expression (but inside the curly braces) excluded.
1147             // But note that we run into a problem.  The original 
1148             // EXPANSION_PATTERN used a reluctant quantifier to match the 
1149             // smallest instance of ${} possible.  But if you have ${'}'} or 
1150             // ${{'key': 'value'}} (maps, cf TAP5-1605) then you run into issues
1151             // b/c the expansion becomes {'key': 'value' which is wrong.
1152             // A fix to use greedy matching with negative lookahead to prevent 
1153             // ${...}...${...} all matching a single expansion is close, but 
1154             // has issues when an expansion is used inside a javascript function
1155             // (see TAP5-1620). The solution is to use the greedy 
1156             // EXPANSION_PATTERN as before to bound the search for a single 
1157             // expansion, then check for {} consistency, ignoring opening and 
1158             // closing braces that occur within '' (the property expression 
1159             // language doesn't support "" for strings). That should include: 
1160             // 'This string has a } in it' and 'This string has a { in it.'
1161             // Note also that the property expression language doesn't support
1162             // escaping the string character ('), so we don't have to worry 
1163             // about that. 
1164             String expression = matcher.group(1);
1165             //count of 'open' braces. Expression ends when it hits 0. In most cases,
1166             // it should end up as 1 b/c "expression" is everything inside ${}, so 
1167             // the following will typically not find the end of the expression.
1168             int openBraceCount = 1;
1169             int expressionEnd = expression.length();
1170             boolean inQuote = false;
1171             for (int i = 0; i < expression.length(); i++)
1172             {
1173                 char c = expression.charAt(i);
1174                 //basically, if we're inQuote, we ignore everything until we hit the quote end, so we only care if the character matches the quote start (meaning we're at the end of the quote).
1175                 //note that I don't believe expression support escaped quotes...
1176                 if (c == EXPANSION_STRING_DELIMITTER)
1177                 {
1178                     inQuote = !inQuote;
1179                     continue;
1180                 } else if (inQuote)
1181                 {
1182                     continue;
1183                 } else if (c == CLOSE_BRACE)
1184                 {
1185                     openBraceCount--;
1186                     if (openBraceCount == 0)
1187                     {
1188                         expressionEnd = i;
1189                         break;
1190                     }
1191                 } else if (c == OPEN_BRACE)
1192                 {
1193                     openBraceCount++;
1194                 }
1195             }
1196             if (expressionEnd < expression.length())
1197             {
1198                 //then we gobbled up some } that we shouldn't have... like the closing } of a javascript
1199                 //function.
1200                 tokenAccumulator.add(new ExpansionToken(expression.substring(0, expressionEnd), textStartLocation));
1201                 //can't just assign to 
1202                 startx = matcher.start(1) + expressionEnd + 1;
1203             } else
1204             {
1205                 tokenAccumulator.add(new ExpansionToken(expression.trim(), textStartLocation));
1206 
1207                 startx = matcher.end();
1208             }
1209         }
1210 
1211         // Catch anything after the final regexp match.
1212 
1213         if (startx < text.length())
1214             tokenAccumulator.add(new TextToken(text.substring(startx, text.length()),
1215                     textStartLocation));
1216     }
1217 
1218     static enum Version
1219     {
1220         T_5_0(5, 0), T_5_1(5, 1), T_5_3(5, 3), T_5_4(5, 4);
1221 
1222         private int major;
1223         private int minor;
1224 
1225 
1226         private Version(int major, int minor)
1227         {
1228             this.major = major;
1229             this.minor = minor;
1230         }
1231 
1232         /**
1233          * Returns true if this Version is the same as, or ordered before the other Version. This is often used to enable new
1234          * template features for a specific version.
1235          */
1236         public boolean sameOrEarlier(Version other)
1237         {
1238             if (other == null)
1239                 return false;
1240 
1241             if (this == other)
1242                 return true;
1243 
1244             return major <= other.major && minor <= other.minor;
1245         }
1246     }
1247 
1248 }